package org.farng.mp3.id3;

import org.farng.mp3.AbstractMP3Tag;
import org.farng.mp3.InvalidTagException;
import org.farng.mp3.MP3File;
import org.farng.mp3.TagConstant;
import org.farng.mp3.TagException;
import org.farng.mp3.TagNotFoundException;
import org.farng.mp3.TagOptionSingleton;
import org.farng.mp3.filename.FilenameTag;
import org.farng.mp3.lyrics3.AbstractLyrics3;
import org.farng.mp3.lyrics3.Lyrics3v2;
import org.farng.mp3.lyrics3.Lyrics3v2Field;

import java.io.IOException;
import java.io.RandomAccessFile;

import java.util.HashMap;
import java.util.Iterator;


/**
 * Title:       ID3v2_4 Description: This class represents an ID3v2.40 tag
 * Copyright:   Copyright (c) 2002 Company:
 *
 * @author Eric Farng
 * @version $Revision: 1.17 $
 */
public class ID3v2_4 extends ID3v2_3 {
    /** DOCUMENT ME! */
    protected boolean footer = false;

    /** DOCUMENT ME! */
    protected boolean tagRestriction = false;

    /** DOCUMENT ME! */
    protected boolean updateTag = false;

    /** DOCUMENT ME! */
    protected byte imageEncodingRestriction = 0;

    /** DOCUMENT ME! */
    protected byte imageSizeRestriction = 0;

    /** DOCUMENT ME! */
    protected byte tagSizeRestriction = 0;

    /** DOCUMENT ME! */
    protected byte textEncodingRestriction = 0;

    /** DOCUMENT ME! */
    protected byte textFieldSizeRestriction = 0;

    /**
     * Creates a new ID3v2_4 object.
     */
    public ID3v2_4() {
        majorVersion = 2;
        revision     = 4;
    }

    public ID3v2_4(ID3v2_4 copyObject) {
        super(copyObject);
        this.footer                   = copyObject.footer;
        this.tagRestriction           = copyObject.tagRestriction;
        this.updateTag                = copyObject.updateTag;
        this.imageEncodingRestriction = copyObject.imageEncodingRestriction;
        this.imageSizeRestriction     = copyObject.imageSizeRestriction;
        this.tagSizeRestriction       = copyObject.tagSizeRestriction;
        this.textEncodingRestriction  = copyObject.textEncodingRestriction;
        this.textFieldSizeRestriction = copyObject.textFieldSizeRestriction;
    }

    /**
     * Creates a new ID3v2_4 object.
     *
     * @param mp3tag DOCUMENT ME!
     */
    public ID3v2_4(AbstractMP3Tag mp3tag) {
        if (mp3tag != null) {
            // if we get a tag, we want to convert to id3v2_4
            // both id3v1 and lyrics3 convert to this type
            // id3v1 needs to convert to id3v2_4 before converting to lyrics3
            if (mp3tag instanceof AbstractID3v2) {
                copyFromID3v2Tag((AbstractID3v2) mp3tag);
            } else if (mp3tag instanceof ID3v1) {
                // convert id3v1 tags.
                ID3v1                  id3tag = (ID3v1) mp3tag;
                ID3v2_4Frame           newFrame;
                AbstractID3v2FrameBody newBody;

                if (id3tag.title.length() > 0) {
                    newBody  = new FrameBodyTIT2((byte) 0, id3tag.title);
                    newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                    frameMap.put(newFrame.getIdentifier(), newFrame);
                }

                if (id3tag.artist.length() > 0) {
                    newBody  = new FrameBodyTPE1((byte) 0, id3tag.artist);
                    newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                    frameMap.put(newFrame.getIdentifier(), newFrame);
                }

                if (id3tag.album.length() > 0) {
                    newBody  = new FrameBodyTALB((byte) 0, id3tag.album);
                    newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                    frameMap.put(newFrame.getIdentifier(), newFrame);
                }

                if (id3tag.year.length() > 0) {
                    newBody  = new FrameBodyTDRC((byte) 0, id3tag.year);
                    newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                    frameMap.put(newFrame.getIdentifier(), newFrame);
                }

                if (id3tag.comment.length() > 0) {
                    newBody  = new FrameBodyCOMM((byte) 0, "ENG", "", id3tag.comment);
                    newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                    frameMap.put(newFrame.getIdentifier(), newFrame);
                }

                if (id3tag.genre >= 0) {
                    String genre = "(" + Byte.toString(id3tag.genre) + ") " + TagConstant.genreIdToString.get(new Long(id3tag.genre));
                    newBody  = new FrameBodyTCON((byte) 0, genre);
                    newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                    frameMap.put(newFrame.getIdentifier(), newFrame);
                }

                if (mp3tag instanceof ID3v1_1) {
                    ID3v1_1 id3tag2 = (ID3v1_1) mp3tag;

                    if (id3tag2.track > 0) {
                        newBody  = new FrameBodyTRCK((byte) 0, Byte.toString(id3tag2.track));
                        newFrame = new ID3v2_4Frame(false, false, false, false, false, false, newBody);
                        frameMap.put(newFrame.getIdentifier(), newFrame);
                    }
                }
            } else if (mp3tag instanceof AbstractLyrics3) {
                // put the conversion stuff in the individual frame code.
                Lyrics3v2 lyric;

                if (mp3tag instanceof Lyrics3v2) {
                    lyric = new Lyrics3v2((Lyrics3v2) mp3tag);
                } else {
                    lyric = new Lyrics3v2(mp3tag);
                }

                Iterator       iterator = lyric.iterator();
                Lyrics3v2Field field;
                ID3v2_4Frame   newFrame;

                while (iterator.hasNext()) {
                    try {
                        field    = (Lyrics3v2Field) iterator.next();
                        newFrame = new ID3v2_4Frame(field);
                        frameMap.put(newFrame.getIdentifier(), newFrame);
                    } catch (InvalidTagException ex) {
                        //ex.printStackTrace();
                    }
                }
            } else if (mp3tag instanceof FilenameTag) {
                copyFromID3v2Tag(((FilenameTag) mp3tag).getId3tag());
            }
        }
    }

    /**
     * Creates a new ID3v2_4 object.
     *
     * @param file DOCUMENT ME!
     *
     * @throws TagException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     */
    public ID3v2_4(RandomAccessFile file)
            throws TagException, IOException {
        this.read(file);
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String getIdentifier() {
        return "ID3v2.40";
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public int getSize() {
        int size = 3 + 2 + 1 + 4;

        if (extended) {
            size += (4 + 1 + 1);

            if (updateTag) {
                size++;
            }

            if (crcDataFlag) {
                size += 5;
            }

            if (tagRestriction) {
                size += 2;
            }
        }

        Iterator           iterator = frameMap.values().iterator();
        AbstractID3v2Frame frame;

        while (iterator.hasNext()) {
            frame = (AbstractID3v2Frame) iterator.next();
            size += frame.getSize();
        }

        return size;
    }

    /**
     * DOCUMENT ME!
     *
     * @param tag DOCUMENT ME!
     */
    public void append(AbstractMP3Tag tag) {
        if (tag instanceof ID3v2_4) {
            this.updateTag                = ((ID3v2_4) tag).updateTag;
            this.footer                   = ((ID3v2_4) tag).footer;
            this.tagRestriction           = ((ID3v2_4) tag).tagRestriction;
            this.tagSizeRestriction       = ((ID3v2_4) tag).tagSizeRestriction;
            this.textEncodingRestriction  = ((ID3v2_4) tag).textEncodingRestriction;
            this.textFieldSizeRestriction = ((ID3v2_4) tag).textFieldSizeRestriction;
            this.imageEncodingRestriction = ((ID3v2_4) tag).imageEncodingRestriction;
            this.imageSizeRestriction     = ((ID3v2_4) tag).imageSizeRestriction;
        }

        super.append(tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param obj DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */

//    public void append(RandomAccessFile file)
//                throws IOException, TagException {
//        ID3v2_4 oldTag;
//
//        try {
//            oldTag = new ID3v2_4(file);
//            oldTag.append(this);
//            oldTag.write(file);
//        } catch (TagNotFoundException ex) {
//            oldTag = null;
//        }
//    }

    /**
     * DOCUMENT ME!
     *
     * @param obj DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean equals(Object obj) {
        if ((obj instanceof ID3v2_4) == false) {
            return false;
        }

        ID3v2_4 object = (ID3v2_4) obj;

        if (this.footer != object.footer) {
            return false;
        }

        if (this.imageEncodingRestriction != object.imageEncodingRestriction) {
            return false;
        }

        if (this.imageSizeRestriction != object.imageSizeRestriction) {
            return false;
        }

        if (this.tagRestriction != object.tagRestriction) {
            return false;
        }

        if (this.tagSizeRestriction != object.tagSizeRestriction) {
            return false;
        }

        if (this.textEncodingRestriction != object.textEncodingRestriction) {
            return false;
        }

        if (this.textFieldSizeRestriction != object.textFieldSizeRestriction) {
            return false;
        }

        if (this.updateTag != object.updateTag) {
            return false;
        }

        return super.equals(obj);
    }

    /**
     * DOCUMENT ME!
     *
     * @param tag DOCUMENT ME!
     */
    public void overwrite(AbstractMP3Tag tag) {
        if (tag instanceof ID3v2_4) {
            this.updateTag                = ((ID3v2_4) tag).updateTag;
            this.footer                   = ((ID3v2_4) tag).footer;
            this.tagRestriction           = ((ID3v2_4) tag).tagRestriction;
            this.tagSizeRestriction       = ((ID3v2_4) tag).tagSizeRestriction;
            this.textEncodingRestriction  = ((ID3v2_4) tag).textEncodingRestriction;
            this.textFieldSizeRestriction = ((ID3v2_4) tag).textFieldSizeRestriction;
            this.imageEncodingRestriction = ((ID3v2_4) tag).imageEncodingRestriction;
            this.imageSizeRestriction     = ((ID3v2_4) tag).imageSizeRestriction;
        }

        super.overwrite(tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @throws TagException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     * @throws TagNotFoundException DOCUMENT ME!
     * @throws InvalidTagException DOCUMENT ME!
     */

//    public void overwrite(RandomAccessFile file)
//                   throws IOException, TagException {
//        ID3v2_4 oldTag;
//
//        try {
//            oldTag = new ID3v2_4(file);
//            oldTag.overwrite(this);
//            oldTag.write(file);
//        } catch (TagNotFoundException ex) {
//            super.overwrite(file);
//        }
//    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @throws TagException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     * @throws TagNotFoundException DOCUMENT ME!
     * @throws InvalidTagException DOCUMENT ME!
     */
    public void read(RandomAccessFile file)
              throws TagException, IOException {
        int    size;
        byte[] buffer = new byte[4];

        file.seek(0);

        if (seek(file) == false) {
            throw new TagNotFoundException(getIdentifier() + " tag not found");
        }

        // read the major and minor @version bytes & flag bytes
        file.read(buffer, 0, 3);

        if ((buffer[0] != 4) || (buffer[1] != 0)) {
            throw new TagNotFoundException(getIdentifier() + " tag not found");
        }

        majorVersion      = buffer[0];
        revision          = buffer[1];
        unsynchronization = (buffer[2] & TagConstant.MASK_V24_UNSYNCHRONIZATION) != 0;
        extended          = (buffer[2] & TagConstant.MASK_V24_EXTENDED_HEADER) != 0;
        experimental      = (buffer[2] & TagConstant.MASK_V24_EXPERIMENTAL) != 0;
        footer            = (buffer[2] & TagConstant.MASK_V24_FOOTER_PRESENT) != 0;

        // read the size
        file.read(buffer, 0, 4);
        size = byteArrayToSize(buffer);

        long filePointer = file.getFilePointer();

        if (extended) {
            // int is 4 bytes.
            int extendedHeaderSize = file.readInt();

            // the extended header must be atleast 6 bytes
            if (extendedHeaderSize <= 6) {
                throw new InvalidTagException("Invalid Extended Header Size.");
            }

            byte numberOfFlagBytes = file.readByte();

            // read the flag bytes
            file.read(buffer, 0, numberOfFlagBytes);
            updateTag      = (buffer[0] & TagConstant.MASK_V24_TAG_UPDATE) != 0;
            crcDataFlag    = (buffer[0] & TagConstant.MASK_V24_CRC_DATA_PRESENT) != 0;
            tagRestriction = (buffer[0] & TagConstant.MASK_V24_TAG_RESTRICTIONS) != 0;

            // read the length byte if the flag is set
            // this tag should always be zero but just in case
            // read this information.
            if (updateTag) {
                int len = file.readByte();
                buffer = new byte[len];
                file.read(buffer, 0, len);
            }

            if (crcDataFlag) {
                // the CRC has a variable length
                int len = file.readByte();
                buffer = new byte[len];
                file.read(buffer, 0, len);
                crcData = 0;

                for (int i = 0; i < len; i++) {
                    crcData <<= 8;
                    crcData += buffer[i];
                }
            }

            if (tagRestriction) {
                int len = file.readByte();
                buffer = new byte[len];
                file.read(buffer, 0, len);

                tagSizeRestriction       = (byte) ((buffer[0] & TagConstant.MASK_V24_TAG_SIZE_RESTRICTIONS) >> 6);
                textEncodingRestriction  = (byte) ((buffer[0] & TagConstant.MASK_V24_TEXT_ENCODING_RESTRICTIONS) >> 5);
                textFieldSizeRestriction = (byte) ((buffer[0] & TagConstant.MASK_V24_TEXT_FIELD_SIZE_RESTRICTIONS) >> 3);
                imageEncodingRestriction = (byte) ((buffer[0] & TagConstant.MASK_V24_IMAGE_ENCODING) >> 2);
                imageSizeRestriction     = (byte) (buffer[0] & TagConstant.MASK_V24_IMAGE_SIZE_RESTRICTIONS);
            }
        }

        ID3v2_4Frame next;
        frameMap = new HashMap();

        // read the frames
        this.fileReadSize      = size;
        ID3v2_2.paddingCounter = 0;

        while ((file.getFilePointer() - filePointer) <= size) {
            try {
                next = new ID3v2_4Frame(file);

                String id = next.getIdentifier();

                if (frameMap.containsKey(id)) {
                    this.duplicateFrameId += (id + "; ");
                    this.duplicateBytes += ((AbstractID3v2Frame) frameMap.get(id)).getSize();
                }

                frameMap.put(id, next);
            } catch (InvalidTagException ex) {
                if (ex.getMessage().equals("Found empty frame")) {
                    this.emptyFrameBytes += 10;
                } else {
                    this.invalidFrameBytes++;
                }

                if ((file.getFilePointer() - filePointer) <= size) {
                    //System.out.println(ex.getMessage());
                }
            }

            ;
        }

        this.padding = ID3v2_2.paddingCounter;

        /**
         * int newSize = this.getSize(); if ((this.padding + newSize - 10) !=
         * size) { System.out.println("WARNING: Tag sizes don't add up");
         * System.out.println("ID3v2.40 tag size : " + newSize);
         * System.out.println("ID3v2.40 padding  : " + this.padding);
         * System.out.println("ID3v2.40 total    : " + (this.padding +
         * newSize)); System.out.println("ID3v2.40 file size: " + size); }
         */
    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     *
     * @throws IOException DOCUMENT ME!
     */
    public boolean seek(RandomAccessFile file)
                 throws IOException {
        byte[] buffer = new byte[3];

        file.seek(0);

        // read the tag if it exists
        file.read(buffer, 0, 3);

        String tag = new String(buffer, 0, 3);

        if (tag.equals("ID3") == false) {
            return false;
        }

        // read the major and minor @version number
        file.read(buffer, 0, 2);

        // read back the @version bytes so we can read and save them later
        file.seek(file.getFilePointer() - 2);

        return ((buffer[0] == 4) && (buffer[1] == 0));
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String toString() {
        Iterator           iterator = frameMap.values().iterator();
        AbstractID3v2Frame frame;
        String             str = getIdentifier() + " " + this.getSize() + "\n";
        str += ("compression              = " + compression + "\n");
        str += ("unsynchronization        = " + unsynchronization + "\n");
        str += ("crcData                  = " + crcData + "\n");
        str += ("crcDataFlag              = " + crcDataFlag + "\n");
        str += ("experimental             = " + experimental + "\n");
        str += ("extended                 = " + extended + "\n");
        str += ("paddingSize              = " + paddingSize + "\n");
        str += ("footer                   = " + footer + "\n");
        str += ("imageEncodingRestriction = " + imageEncodingRestriction + "\n");
        str += ("imageSizeRestriction     = " + imageSizeRestriction + "\n");
        str += ("tagRestriction           = " + tagRestriction + "\n");
        str += ("tagSizeRestriction       = " + tagSizeRestriction + "\n");
        str += ("textEncodingRestriction  = " + textEncodingRestriction + "\n");
        str += ("textFieldSizeRestriction = " + textFieldSizeRestriction + "\n");
        str += ("updateTag                = " + updateTag + "\n");

        while (iterator.hasNext()) {
            frame = (ID3v2_4Frame) iterator.next();
            str += (frame.toString() + "\n");
        }

        return str + "\n";
    }

    /**
     * DOCUMENT ME!
     *
     * @param tag DOCUMENT ME!
     */
    public void write(AbstractMP3Tag tag) {
        if (tag instanceof ID3v2_4) {
            this.updateTag                = ((ID3v2_4) tag).updateTag;
            this.footer                   = ((ID3v2_4) tag).footer;
            this.tagRestriction           = ((ID3v2_4) tag).tagRestriction;
            this.tagSizeRestriction       = ((ID3v2_4) tag).tagSizeRestriction;
            this.textEncodingRestriction  = ((ID3v2_4) tag).textEncodingRestriction;
            this.textFieldSizeRestriction = ((ID3v2_4) tag).textFieldSizeRestriction;
            this.imageEncodingRestriction = ((ID3v2_4) tag).imageEncodingRestriction;
            this.imageSizeRestriction     = ((ID3v2_4) tag).imageSizeRestriction;
        }

        super.write(tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param file DOCUMENT ME!
     *
     * @throws IOException DOCUMENT ME!
     */
    public void write(RandomAccessFile file)
               throws IOException {
        int          size;
        String       str;
        Iterator     iterator;
        ID3v2_4Frame frame;
        byte[]       buffer = new byte[6];

        MP3File      mp3 = new MP3File();
        mp3.seekMP3Frame(file);

        long mp3start = file.getFilePointer();

        if(!TagOptionSingleton.getInstance().isId3v2PaddingCopyTag()){
            // write zero tag
            byte[] zerobuffer = new byte[(int)mp3start];
            file.seek(0);
            file.write(zerobuffer, 0, zerobuffer.length);
        }
        
        file.seek(0);
        str = "ID3";

        for (int i = 0; i < str.length(); i++) {
            buffer[i] = (byte) str.charAt(i);
        }

        buffer[3] = 4;
        buffer[4] = 0;

        if (unsynchronization) {
            buffer[5] |= TagConstant.MASK_V24_UNSYNCHRONIZATION;
        }

        if (extended) {
            buffer[5] |= TagConstant.MASK_V24_EXTENDED_HEADER;
        }

        if (experimental) {
            buffer[5] |= TagConstant.MASK_V24_EXPERIMENTAL;
        }

        if (footer) {
            buffer[5] |= TagConstant.MASK_V24_FOOTER_PRESENT;
        }

        file.write(buffer);

        // write size
        file.write(sizeToByteArray((int) mp3start - 10));

        if (extended) {
            size = 6;

            if (updateTag) {
                size++;
            }

            if (crcDataFlag) {
                size += 5;
            }

            if (tagRestriction) {
                size += 2;
            }

            file.writeInt(size);

            file.writeByte(1); // always 1 byte of flags in this tag

            buffer[0] = 0;

            if (updateTag) {
                buffer[0] |= TagConstant.MASK_V24_TAG_UPDATE;
            }

            if (crcDataFlag) {
                buffer[0] |= TagConstant.MASK_V24_CRC_DATA_PRESENT;
            }

            if (tagRestriction) {
                buffer[0] |= TagConstant.MASK_V24_TAG_RESTRICTIONS;
            }

            file.writeByte(buffer[0]);

            if (updateTag) {
                file.writeByte(0);
            }

            // this can be variable length, but this is easier
            if (crcDataFlag) {
                file.writeByte(4);
                file.writeInt(crcData);
            }

            if (tagRestriction) {
                /**
                 * @todo we need to finish this
                 */
                file.writeByte(1);
                buffer[0] = (byte) 0;

                if (tagRestriction) {
                    buffer[0] |= TagConstant.MASK_V24_TAG_SIZE_RESTRICTIONS;
                }

                //if (textEncodingRestriction
                file.writeByte(tagSizeRestriction);
                file.writeByte(textEncodingRestriction);
                file.writeByte(textFieldSizeRestriction);
                file.writeByte(imageEncodingRestriction);
                file.writeByte(imageSizeRestriction);
                file.writeByte(buffer[0]);
            }
        }

        // write all frames
        iterator = frameMap.values().iterator();

        while (iterator.hasNext()) {
            frame = (ID3v2_4Frame) iterator.next();
            frame.write(file);
        }

        if (this.getSize() != file.getFilePointer()) {
            //System.out.println("ID3v2.20 size didn't match up while writing.");
            //System.out.println("this.getsize()     = " + this.getSize());
            //System.out.println("size (filePointer) = " + file.getFilePointer());
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param mp3tag DOCUMENT ME!
     */
    private void copyFromID3v2Tag(AbstractID3v2 mp3tag) {
        // if the tag is id3v2_4
        if (mp3tag instanceof ID3v2_4) {
            ID3v2_4 tag = (ID3v2_4) mp3tag;
            this.footer                   = tag.footer;
            this.tagRestriction           = tag.tagRestriction;
            this.updateTag                = tag.updateTag;
            this.imageEncodingRestriction = tag.imageEncodingRestriction;
            this.imageSizeRestriction     = tag.imageSizeRestriction;
            this.tagSizeRestriction       = tag.tagSizeRestriction;
            this.textEncodingRestriction  = tag.textEncodingRestriction;
            this.textFieldSizeRestriction = tag.textFieldSizeRestriction;
        }

        if (mp3tag instanceof ID3v2_3) {
            // and id3v2_4 tag is an instance of id3v2_3 also ...
            ID3v2_3 id3tag = (ID3v2_3) mp3tag;
            this.extended     = id3tag.extended;
            this.experimental = id3tag.experimental;
            this.crcDataFlag  = id3tag.crcDataFlag;
            this.crcData      = id3tag.crcData;
            this.paddingSize  = id3tag.paddingSize;
        }

        if (mp3tag instanceof ID3v2_2) {
            ID3v2_2 id3tag = (ID3v2_2) mp3tag;
            this.compression       = id3tag.compression;
            this.unsynchronization = id3tag.unsynchronization;
        }

        AbstractID3v2      id3tag = (AbstractID3v2) mp3tag;
        Iterator           iterator = id3tag.frameMap.values().iterator();
        AbstractID3v2Frame frame;
        ID3v2_4Frame       newFrame;

        while (iterator.hasNext()) {
            frame    = (AbstractID3v2Frame) iterator.next();
            newFrame = new ID3v2_4Frame(frame);
            frameMap.put(newFrame.getIdentifier(), newFrame);
        }
    }
}